PVI – Perpendicular Vegetation Index

PVI is a soil-line–based vegetation index that measures the perpendicular distance between a pixel and the soil line in Red–NIR space, improving separation between bare soil and green vegetation under varying soil brightness.

1. Scientific Definition

The Perpendicular Vegetation Index (PVI) is a spectral index that uses the geometric distance of a pixel from the soil line in the Red–Near-InfraRed (NIR) reflectance space. The soil line describes the linear relationship between Red and NIR reflectance for bare soils:

NIR = a × Red + b

PVI quantifies how far a pixel lies above this soil line. Higher PVI values indicate stronger vegetation signal (higher NIR and lower Red compared to soil), while values near zero correspond to bare soil or very sparse vegetation.

Formula

A common formulation of PVI is:

PVI = (NIR − a × Red − b) / √(1 + a²) Dimensionless (relative distance)

where:

  • NIR = Near-InfraRed reflectance
  • Red = Red reflectance
  • a = slope of the soil line
  • b = intercept of the soil line

Typical Interpretation (qualitative)

PVI (relative) Interpretation
≈ 0 Bare soil, exposed ground, or very sparse vegetation close to the soil line
Low positive Low–moderate vegetation cover, grassland or mixed soil–vegetation pixels
High positive Dense and healthy vegetation with strong NIR and low Red reflectance
Negative Bright non-vegetated surfaces or atypical spectra below the soil line

Key Applications

  • Separating vegetation from soil in areas with strong soil background effects
  • Vegetation mapping in semi-arid and arid regions
  • Complementing NDVI/SAVI when soil brightness varies strongly
  • Input feature in land cover classification and agricultural monitoring

2. Data & Bands for PVI

Common Sensors & Bands

  • Sentinel-2 (ESA) – 10 m
    • Red: B4 (~665 nm)
    • NIR: B8 (~842 nm)
  • Landsat 8/9 OLI – 30 m
    • Red: B4
    • NIR: B5

Soil Line Parameters

In practice, a and b should be estimated from bare soil pixels in your scene by fitting a linear regression in Red–NIR space. For demonstration purposes, many examples use approximate values such as:

  • a ≈ 1.0
  • b ≈ 0.0 – 0.05 (depending on sensor and scene)

Good Practice

  • Use atmospherically corrected surface reflectance products (e.g. COPERNICUS/S2_SR).
  • Filter scenes by cloud percentage and apply cloud masking.
  • Estimate the soil line using bare soil samples for more accurate PVI.
  • Clip the final PVI raster to your AOI before exporting.

Palette Suggestion

A simple PVI palette similar to vegetation indices: [ "#440154", "#3b528b", "#21908c", "#5dc963", "#fde725" ]

3. Google Earth Engine Code – PVI for Any AOI

Steps: open code.earthengine.google.com → New Script → paste the code → draw your AOI as geometry on the map → click Run. Then export PVI as GeoTIFF to Google Drive.

// PVI for any Area of Interest (AOI) using Sentinel-2 SR
// -------------------------------------------------------
// 1) Go to: https://code.earthengine.google.com
// 2) Click "New Script" and paste this code.
// 3) On the map: draw your AOI (Polygon/Rectangle).
//    It will appear as a variable named 'geometry' in the left panel.
// 4) Click "Run" to display PVI.
// 5) In the Tasks tab, click "Run" to export PVI to Google Drive.

// -------------------------------------------------------
// 1. Define Area of Interest (AOI)
// -------------------------------------------------------
var roi = geometry;  // Make sure a 'geometry' object exists in the left panel

// Center the map on the AOI
Map.centerObject(roi, 11);

// -------------------------------------------------------
// 2. Define time range
// -------------------------------------------------------
var startDate = '2023-01-01';
var endDate   = '2023-12-31';

// -------------------------------------------------------
// 3. Load Sentinel-2 Surface Reflectance collection
// -------------------------------------------------------
var s2 = ee.ImageCollection('COPERNICUS/S2_SR')
  .filterBounds(roi)
  .filterDate(startDate, endDate)
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20));

// Create a median composite and clip to AOI
var image = s2.median().clip(roi);

// -------------------------------------------------------
// 4. Define soil line parameters (example values)
//    Ideally, estimate 'a' and 'b' from bare soil samples
// -------------------------------------------------------
var a = 1.0;   // soil line slope
var b = 0.05;  // soil line intercept

// -------------------------------------------------------
// 5. Compute PVI
//    PVI = (NIR - a * Red - b) / sqrt(1 + a^2)
// -------------------------------------------------------
var pvi = image.expression(
  '(NIR - a * RED - b) / sqrt(1 + a * a)',
  {
    'NIR': image.select('B8'),  // NIR band (Sentinel-2)
    'RED': image.select('B4'),  // Red band
    'a':   a,
    'b':   b
  }
).rename('PVI');

// -------------------------------------------------------
// 6. Visualization on the map
// -------------------------------------------------------
var pviVis = {
  min: -0.5,
  max:  0.8,
  palette: [
    '#440154', // low / near soil / negative
    '#3b528b',
    '#21908c',
    '#5dc963',
    '#fde725'  // high vegetation signal
  ]
};

// Add PVI layer to the map
Map.addLayer(pvi, pviVis, 'PVI (Sentinel-2)', true);

// Optionally, also show a true color composite for context
var s2_rgb = ee.ImageCollection('COPERNICUS/S2_SR')
  .filterBounds(roi)
  .filterDate(startDate, endDate)
  .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
  .select(['B4','B3','B2'])  // RGB
  .median()
  .clip(roi);

Map.addLayer(s2_rgb, {min:0, max:3000}, 'True Color (RGB)', false);

// -------------------------------------------------------
// 7. Export PVI as GeoTIFF to Google Drive
// -------------------------------------------------------
Export.image.toDrive({
  image: pvi,
  description: 'PVI_Export',
  fileNamePrefix: 'PVI_Export',
  region: roi,
  scale: 10,       // Sentinel-2 native resolution for B8/B4
  crs: 'EPSG:4326',
  maxPixels: 1e13
});

// End of script